Preprocessor - A 2 Part Tutorial

Part II

© 2006, Rich Ries

author contact:

Rich.Ries@Honeywell.com

Home

Tip Corner: ByRef

API Corner: File Download

Working with Strings 3

Stylebits Corner: Dialogs

Eddie's Lessons, v.10

Liberty Basic Wiki

Preprocessor 1

Preprocessor 2

Find Folder

Multiple Listboxes

Newsletter help

Index


As promised in the previous part, B-Prep will start off simple, and get more complex as time goes on. The time for complexity has arrived.

File: BPREP-4.BAS

As first designed, the #if-#else-#endif preprocessor command set does not allow nesting. That is,

#if A
	Some Code
  #if B
	More Code
  #else
    Different Code
  #endif
#endif

is an illegal construct. If A is undefined, then the preprocessor will gobble lines until it hits "#else", at which point it will stop gobbling. If A is defined, then the preprocessor will work as expected. One possible way to solve this problem is to keep track of which #endif/#else goes with which #if:

#if A
	Some Code
  #if B
	More Code
  #else B
    Different Code
  #endif B
#endif A

This would enforce a better style. A lot of my time is spent looking at others' code, and trying to figure out which #endif goes with which #if. A better way is to run Analyze on the code blocks. As it stands, Analyze works on one line of code, and returns to its caller. The caller loops, reads in the next line of code, and calls Analyze. Rather than adding code for this to each command that needs it, we'll convert the caller into a subroutine, and call it whenever we get a block of code.

Go to the [Top] label, and remove it and the line "goto [Top]". Move the line "call GetNextLine ' Loads RawData$" to the start of Analyze. Now we need to add a loop to Analyze. You could use the "[Top]"/"goto [Top]", but I got a little fancier, and used "DO ... LOOP WHILE TRUE"

'---------------------------------------------------
sub Analyze

	do
		call GetNextLine ' Loads RawData$
		if instr(RawData$,"#") <> 0 then
			' Trim any leading spaces
			PPCmdStr$ = trim$(RawData$)
			' make sure the # is first in the line
			if left$(PPCmdStr$,1) = "#" then
				' Get the command
				PPCmd$ = word$(PPCmdStr$,1)
				' Trim it
				PPCmdStr$ = trim$(mid$(PPCmdStr$,len(PPCmd$)+1))
				select case PPCmd$
					case "#define"					
						call DoDefine PPCmdStr$
					case "#undefine"					
						call DoUndefine PPCmdStr$
					case "#ifdef"
						call DoIfDef PPCmdStr$
					case "#else"
						call DoElse
                        exit do ' Get out of the #IFDEF Analyze
					case "#endif"
						call DoEndIf
                        exit do ' Get out of the #IFDEF Analyze
' For Testing
					case "#end"
						exit do
						
				end select
			end if
		else
			call write RawData$
		end if
	loop while TRUE
end sub

Next, we'll need to adjust the "#if" command, and modify Analyze. "#endif" does nothing, and we want to keep it that way. "#else" gulps lines, and we want to keep that, too.

DO loop at its heart is a miniature Analyzer. This means that we can replace the mini-Analyzer with a call to the real one, and add a "gulper" in the case that the #if is undefined (in which case we want to ignore all lines until #else or #endif are found). If #else is found, we want to start analyzing the block, so we add a call to Analyze in the "#else" portion:
'---------------------------------------------------
sub DoIfDef TheName$
	Du = IsDefined(TheName$)
	if Du <> 0 then
		' It's defined
		call Analyze
	else ' Gulp until we find "#else" or "#endif"
		do
			call GetNextLine
			Du$ = word$(RawData$,1)
			if Du$ = "#else" then
				call Analyze
				exit do
			end if
		loop until Du$ = "#endif"
	
	end if
end sub

Add the code, and test it out. You should now be able to #define a variable within a #if block, something you could not do previously.

File: BPREP-5.BAS

The next command will be "#include". To start this, we'll need to modify the initialization subroutine as well as the input and output subroutines and the error handler. We'll also need a test program or two. We'll start with the testers:

' File Test5-1.bas

FOR n = 1 TO 10
	PRINT "Who are you?"
NEXT n

There's no #include command yet -- we're just seeing how well we can read & write a single file.

' File: Test5-2.bas

PRINT "In Test1.mod"

Now, we'll change InitializeSystem by adding an OPEN command. File handles are naturally Global, so that saves a little work. We won't do any writes to a file yet -- we want to see where we're going.

'---------------------------------------------------
sub InitializeSystem
	FILEDIALOG "File to Process", "*.*", FilePath$
	InHandle$ = "1"
	open FilePath$ for input as #InHandle$
	' Get the File Path
	for n = len(FilePath$) to 1 step -1
		Du$ = mid$(FilePath$,n,1)
		if Du$ = "\" then
			Path$ = left$(FilePath$,n)
			exit for
		end if
	next n
    call write "' BASIC Preprocessor"
    call write "'"
end sub

'---------------------------------------------------
sub DoError ErrMsg$
	call write ErrMsg$
	close #InHandle$
	input A$
	end
end sub

'+++

We'll have to modify GetNextLine to read from files. So far, we're just opening and closing one file, so we don't need to get too fancy; however, note that I'm changing GetNextLine from a SUB to a FUNCTION, to flag when we've hit the end of the file.

'---------------------------------------------------
function GetNextLine()
' Eats up blank lines
    do
[CheckEOF]	
		if eof(#InHandle$) <> 0 then
			if FHAIndex > 0 then
				close #InHandle$
				InHandle$ = FileHandleArray$(FHAIndex)
				FHAIndex = FHAIndex - 1
				goto [CheckEOF]
			else ' FHAIndex
				close #InHandle$
				RawData$ = ""
				GetNextLine = FALSE
				exit do
			end if ' FHAIndex
		end if ' eof

		line input #InHandle$, RawData$
			
		Du = instr(RawData$, COMMENT$)
		if Du <> 0 Then
			RawData$ = left$(RawData$,Du-1)
		end if
		RawData$ = RTrim$(RawData$)
		GetNextLine = TRUE
	loop while RawData$ = ""
end function

Run the code. You should see the File Dialog come up. Select "TEST.BAS", and you'll see the contents of TEST1.BAS getting printed on the console. Now, we'll need to add the "include" command to Analyze, and a subroutine for it, along with a function to test if the file exists.

'---------------------------------------------------
sub DoInclude TheFileName$
	Du = instr(TheFileName$,chr$(34))
	
	' Test for quotes
	if Du = 0 then 
		call DoError "Include filenames must have "+chr$(34)+"s"
	end if
	File$ = mid$(TheFileName$,Du+1)
	Du = instr(File$,chr$(34))
	
	' Test for quotes
	if Du = 0 then 
		call DoError "Include filenames must have "+chr$(34)+"s"
	end if
	File$ = left$(File$,Du-1)

	if File$ = "" then
		call DoError "Include must have a filename."
	else
		' Load InHandle$ into next entry of array
		FHAIndex = FHAIndex + 1
		if FHAIndex > MAXFILES then
			call DoError "Maximum number of open files exceeded."
		end if
		FileHandleArray$(FHAIndex) = InHandle$
		' Iniz file handle
		InHandle$ = "1"
		' Add current path if none is shown
		if instr(File$,"/") = 0 then
			FilePath$ = Path$+File$
		end if
		' Open the file
		if FileExists(File$) then
			open FilePath$ for input as #InHandle$
		else
			call DoError File$+" does not exist in "+Path$
		end if
	end if
end sub

'---------------------------------------------------
function FileExists(File$)
	files Path$, File$, FileInfo$()
	FileExists = val(FileInfo$(0, 0))  'non zero is true
end function

Note that there is a lot of testing going on in the function. I'd rather have the software tell me why it failed rather than the OS error message, which is usually less than helpful! Also, I've limited the maximum number of files that can be open simultaneously to 10, which should be more than enough. If not, just change MAXFILES.

File: BPREP-6.BAS

We'll wind this up with modifying the way the outputs are written. Up to this point, all output has gone to the main window. This is OK for testing, but for serious work we need to put the preprocessor output to a file, and use some popup windows for the error messages.

First, we'll turn off the main window, and add a global name for the output handle. Call it "OutHandle"!

NOMAINWIN
'====== GLOBALS =======
global TRUE, FALSE, TRUE$, FALSE$
global COMMENT$, TAB$

FALSE = 0
TRUE = -1
FALSE$ = "0"
TRUE$ = "-1"
COMMENT$ = "'"
TAB$ = CHR$(9)

global RawData$

' Definitions
global MAXDEFS, Definition$, NextDef
MAXDEFS = 1000
DIM Definition$(MAXDEFS)
NextDef = 1

' Include
' Most files open at one time
global MAXFILES
' Working file handle
global InHandle$
' Fully-qualified file name
global FilePath$
' Path (from FilePath$)
global Path$

MAXFILES = 10
' Holds open handles
DIM FileHandleArray$(MAXFILES)
' Points to current FileHandleArray$ entry
global FHAIndex
FHAIndex = 0
' Used to see if file exists
dim FileInfo$(10, 10)

Then we'll need to add a FILEDIALOG to InitializeSystem to get the output file name, and close the handle in GetNextLine().

'---------------------------------------------------
sub InitializeSystem
	FILEDIALOG "File to Process", "*.*", FilePath$
	InHandle$ = "1"
	open FilePath$ for input as #InHandle$
	' Get the File Path
	for n = len(FilePath$) to 1 step -1
		Du$ = mid$(FilePath$,n,1)
		if Du$ = "\" then
			Path$ = left$(FilePath$,n)
			exit for
		end if
	next n

	FILEDIALOG "File to Write to", "*.*", OFilePath$
	open OFilePath$ for output as #OutHandle

    call write "' BASIC Preprocessor"
    call write "'"
end sub

'---------------------------------------------------
function GetNextLine()
' Eats up blank lines
    do
[CheckEOF]	
		if eof(#InHandle$) <> 0 then
			if FHAIndex > 0 then
				close #InHandle$
				InHandle$ = FileHandleArray$(FHAIndex)
				FHAIndex = FHAIndex - 1
				goto [CheckEOF]
			else ' FHAIndex
				close #InHandle$
				close #OutHandle
				RawData$ = ""
				GetNextLine = FALSE
				exit do
			end if ' FHAIndex
		end if ' eof

		line input #InHandle$, RawData$
			
		Du = instr(RawData$, COMMENT$)
		if Du <> 0 Then
			RawData$ = left$(RawData$,Du-1)
		end if
		RawData$ = RTrim$(RawData$)
		GetNextLine = TRUE
	loop while RawData$ = ""
end function

"write" will need to be changed to send output to the output file handle:

'---------------------------------------------------
sub write aString$
    print #OutHandle, aString$
end sub
and we can remove the "#end" case from Analyze.
'---------------------------------------------------
sub Analyze
	if GetNextLine() = TRUE then ' Loads RawData$
		do		
			if instr(RawData$,"#") <> 0 then
				' Trim any leading spaces
				PPCmdStr$ = trim$(RawData$)
				' make sure the # is first in the line
				if left$(PPCmdStr$,1) = "#" then
					' Get the command
					PPCmd$ = word$(PPCmdStr$,1)
					' Trim it
					PPCmdStr$ = trim$(mid$(PPCmdStr$,len(PPCmd$)+1))
					select case PPCmd$
						case "#define"					
							call DoDefine PPCmdStr$
						case "#undefine"					
							call DoUndefine PPCmdStr$
						case "#ifdef"
							call DoIfDef PPCmdStr$
						case "#else"
							call DoElse
                            exit do ' Get out of the #IFDEF Analyze
						case "#endif"
							call DoEndIf
                            exit do ' Get out of the #IFDEF Analyze
						case "#include"
							call DoInclude PPCmdStr$
					end select
				end if
			else
				call write RawData$
			end if
		loop while GetNextLine() = TRUE
	end if
end sub

Finally, we'll send the error messages to a notice window, and add a completion notice to the end.

'---------------------------------------------------
sub DoError ErrMsg$
	notice "Fatal Error!" + chr$(13) + ErrMsg$
	close #InHandle$
	close #OutHandle
	end
end sub

'+++

	notice "Preprocessing is Complete."
end

Once this is done, we can try running tests. The test files are Test6-1.bas through Test6-4.bas. Double-click on Test6-1.bas to select it for input, then enter a filename for the results (output) file. I use "Du.bas" -- a carry-over from math class when Du was a dummy variable.

Once the preprocessing is finished, check Du.bas to make sure that what you've expected to happen has indeed happened.

Enjoy!